import { profile } from '@/utils/decorators';
import { $render, Logger } from '@/utils/logger';
import { FloodFill } from './floodfill';
import { minCutWalls } from './minCut';
import RoomVisual2 from './visual';

/**
 * note! 用 wasm 重新 FloodFill.do && 用优先队列优化 Array.sort
 */

/**
 * 规划器的上下文包装器，仅限内部传递信息
 */
interface PlanContext {
    room: Room;
    centerPos?: RoomPosition;
    spawnPos?: RoomPosition;
    terrain: RoomTerrain;
    centerFlood?: PathFinder['CostMatrix'];
    spawnFlood?: PathFinder['CostMatrix'];
    controllerFlood?: PathFinder['CostMatrix'];
    roadPos?: `${number}/${number}`[];
}
/**
 * 建造顺序存储器
 */
declare global {
    type BuildSequence = {
        [type in BuildableStructureConstant]?: `${number}/${number}`[];
    };
}

// 布局 ===========================================================================

const HATCHERY_LAYOUT: RoomBlueprint = [
    /*
    *  -----
    * |SeeeS|
    * |e e e|
    * |celec|
    * |e e e|
    * |eeSee|
    *  -----
    * 
    * 注：null 与 undefined
    * - undefined 表示这里是空的，可以使墙壁
    * - null 表示这个位置必须保留
    */
    [undefined, 'road', 'road', 'road', 'road', 'road', undefined],
    ['road', 'spawn', 'extension', 'extension', 'extension', 'spawn', 'road'],
    ['road', 'extension', null, 'extension', null, 'extension', 'road'],
    ['road', 'container', 'extension', 'link', 'extension', 'container', 'road'],
    ['road', 'extension', null, 'extension', null, 'extension', 'road'],
    ['road', 'extension', 'extension', 'spawn', 'extension', 'extension', 'road'],
    [undefined, 'road', 'road', 'road', 'road', 'road', undefined]
];

const CENTER_LAYOUT: RoomBlueprint = [
    [undefined, 'road', 'road', 'road', undefined],
    ['road', 'powerSpawn', 'factory', 'observer', 'road'],
    ['road', 'link', null, 'storage', 'road'],
    ['road', 'nuker', 'terminal', 'road', undefined],
    [undefined, 'road', 'road', 'road', undefined]
];

const EXTENSION_LAYOUT: RoomBlueprint = [
    [undefined, undefined, 'road', undefined, undefined],
    [undefined, 'road', 'extension', 'road', undefined],
    ['road', 'extension', 'extension', 'extension', 'road'],
    [undefined, 'road', 'extension', 'road', undefined],
    [undefined, undefined, 'road', undefined, undefined],
];

const LAB_CLUSTER_LAYOUT: RoomBlueprint = [
    /*
     * \XX
     * X\XX
     * XX\X
     *  XX\
    */
    [null, null, null, undefined, undefined],
    [null, 'road', 'lab', 'lab', undefined, undefined],
    [undefined, 'lab', 'road', 'lab', 'lab', undefined],
    [undefined, 'lab', 'lab', 'road', 'lab', undefined],
    [undefined, undefined, 'lab', 'lab', 'road'],
    [undefined, undefined, undefined, null, null]
]

// ===============================================================================

const floodFill = FloodFill.do;

// ===============================================================================

export class RoomPlanner {

    getEmptyBlueprint() {
        const result: RoomBlueprint = [];
        const ROOM_SIZE = 50;
        for (let i = 0; i < ROOM_SIZE; ++i) {
            result[i] = new Array(ROOM_SIZE).fill(undefined);
        }
        return result;
    }

    /**
     * 把模板应用在一个蓝图上，如果原来有建筑就跳过
     * @param original 原始蓝图
     * @param x 应用的横坐标
     * @param y 应用的纵坐标
     * @param template 模板
     */
    private applyBlueprint(original: RoomBlueprint, x: number, y: number, template: RoomBlueprint, seq: BuildSequence) {
        for (let dy = 0; dy < template.length; ++dy) {
            for (let dx = 0; dx < template[dy].length; ++dx) {
                if (template[dy][dx] === undefined || original[x + dx][y + dy] !== undefined) {
                    continue;
                }
                original[x + dx][y + dy] = template[dy][dx];
                const structureType = template[dy][dx];
                if (!seq[structureType]) {
                    seq[structureType] = [];
                }
                seq[structureType].push(`${x + dx}/${y + dy}`);
            }
        }
    }

    @profile
    private canApply(original: RoomBlueprint, x: number, y: number, template: RoomBlueprint, terrain: RoomTerrain) {
        for (let dy = 0; dy < template.length; ++dy) {
            for (let dx = 0; dx < template[dy].length; ++dx) {
                // 不用考虑的位置
                if (template[dy][dx] === undefined) {
                    continue;
                }
                // 访问越界，放不下
                if (x + dx < 2 || y + dy < 2 || x + dx > 47 || y + dy > 47) {
                    return false;
                }
                // 已有建筑或该位置是墙壁
                if (original[x + dx][y + dy] !== undefined || terrain.get(x + dx, y + dy) === TERRAIN_MASK_WALL) {
                    // 特殊情况，如果要放的建筑是路，而原来也是路，判断可以放
                    if (!(original[x + dx][y + dy] === 'road' && template[dy][dx] === 'road')) {
                        return false;
                    }
                }
                // 保留位置
                if (template[dy][dx] === null && terrain.get(x + dx, x + dy) === TERRAIN_MASK_WALL) {
                    return false;
                }
            }
        }
        return true;
    }

    @profile
    plan(room: Room) {
        let result: RoomBlueprint = this.getEmptyBlueprint();
        const context: PlanContext = { room, terrain: room.getTerrain() };
        const sequence: BuildSequence = {};
        const beginTime = Game.cpu.getUsed();
        // 先填充孵化中心，直接按照 spawn 的位置完成
        context.spawnPos = room.getSpawns().sort((a, b) => a.pos.x - b.pos.x)[0].pos;
        context.spawnFlood = floodFill(room, [context.spawnPos]);
        context.controllerFlood = floodFill(room, [room.getController().pos])
        this.applyBlueprint(result, context.spawnPos.x - 1, context.spawnPos.y - 1, HATCHERY_LAYOUT, sequence);
        // 保护控制器，同时防止防止建筑
        result = this.protectController(result, context);
        // 规划中央集群
        const centerPos = this.getCenterPos(result, context);
        context.centerPos = <RoomPosition>centerPos;
        context.centerFlood = floodFill(room, [context.centerPos]);
        if (centerPos.x && centerPos.y)
            this.applyBlueprint(result, centerPos.x - 2, centerPos.y - 2, CENTER_LAYOUT, sequence);
        else
            throw new Error('无法规划中央集群的位置');
        // 填充 extensions
        result = this.fillExtensions(result, context, sequence);
        // 规划 lab 集群
        const labPos = this.getLabClusterPos(result, context);
        if (labPos.x && labPos.y)
            this.applyBlueprint(result, labPos.x, labPos.y, LAB_CLUSTER_LAYOUT, sequence);
        else
            throw new Error('无法规划实验室集群的位置');
        // 放置塔
        result = this.putTowers(result, context);
        // 最小割
        result = this.getRampartBlueprint(result, context);
        // 铺路
        result = this.paveRoads(result, context, <RoomPosition>labPos);
        const deltaTime = Game.cpu.getUsed() - beginTime;
        return {
            blueprint: result,
            roads: context.roadPos,
            deltaTime,
            spawnPos: context.spawnPos,
            sequence
        };
    }

    @profile
    private getCenterPos(lastBlueprint: RoomBlueprint, context: PlanContext) {
        const { room } = context;
        // 从所有的资源点开始 flood fill，然后累加矩阵
        const sourceMatrixes = room.getSources()
            .map(s => floodFill(room, [s.pos]));
        const controllerMatrix = floodFill(room, [room.getController().pos]);
        context.controllerFlood = controllerMatrix;
        // 注：这里不考虑 mineral，因为 mineral 的采集时间间隔大，且仅在中后期
        const matrixSum = new PathFinder.CostMatrix;
        // 累加矩阵
        const addMatrix = (a: CostMatrix, b: CostMatrix, factor = 1) => {
            for (let i = 0; i < 50; ++i) {
                for (let j = 0; j < 50; ++j) {
                    a.set(i, j, a.get(i, j) + b.get(i, j) * factor);
                }
            }
        }
        sourceMatrixes.forEach(m => addMatrix(matrixSum, m));
        addMatrix(matrixSum, context.controllerFlood);
        addMatrix(matrixSum, context.spawnFlood, 3);
        // 筛选最小值
        const candidates: { pos: RoomPosition, val: number }[] = [];
        // 把 pos 和 floodfill 的值放入数组中
        for (let y = 0; y < 50; ++y) {
            for (let x = 0; x < 50; ++x) {
                candidates.push({ pos: new RoomPosition(x, y, room.name), val: matrixSum.get(x, y) })
            }
        }
        // 排序
        candidates.sort((a, b) => a.val - b.val);
        // 遍历待选定的点，检查是否放得下
        for (const canpos of candidates) {
            if (this.canApply(lastBlueprint, canpos.pos.x - 2, canpos.pos.y - 2, CENTER_LAYOUT, room.getTerrain())) {
                return canpos.pos;
            }
        }
        return { x: undefined, y: undefined };
    }

    private protectController(last: RoomBlueprint, context: PlanContext) {
        const { x, y } = context.room.getController().pos;
        for (const dx of [-1, 0, 1]) {
            for (const dy of [-1, 0, 1]) {
                if (context.terrain.get(x + dx, y + dy) !== TERRAIN_MASK_WALL) {
                    last[x + dx][y + dy] = 'rampart';
                }
            }
        }
        return last;
    }

    @profile
    private getLabClusterPos(last: RoomBlueprint, context: PlanContext) {
        const { room, centerPos } = context;
        const wallPos: RoomPosition[] = [];
        const terrain = context.terrain;
        for (let x = 0; x < 50; ++x) {
            for (let y = 0; y < 50; ++y) {
                if (terrain.get(x, y) === TERRAIN_MASK_WALL) {
                    wallPos.push(new RoomPosition(x, y, room.name));
                }
            }
        }
        // 从墙壁处 flood fill，找一块足够大的空地
        const wallMatrix = floodFill(room, wallPos);
        const candidates: RoomPosition[] = [];
        for (let x = 0; x < 50; ++x) {
            for (let y = 0; y < 50; ++y) {
                if (wallMatrix.get(x, y) >= 3) {
                    candidates.push(new RoomPosition(x, y, room.name));
                }
            }
        }
        // 排序，不会离中心太久，防止核弹打击
        candidates.sort((a, b) => a.getRangeTo(centerPos) - b.getRangeTo(centerPos));
        // 查找一块空地
        for (const candidate of candidates) {
            if (this.canApply(last, candidate.x, candidate.y, LAB_CLUSTER_LAYOUT, room.getTerrain())) {
                return candidate;
            }
        }
        return { x: undefined, y: undefined };
    }

    private fillExtensions2(last: RoomBlueprint, context: PlanContext) {
        const candidates: RoomPosition[] = [];
        for (let i = 0; i < 50; ++i) {
            for (let j = 0; j < 50; ++j) {
                if ((i + j) % 2 === 0) {
                    candidates.push(new RoomPosition(i, j, context.room.name));
                }
            }
        }
        candidates.sort((a, b) => a.getRangeTo(context.spawnPos) - b.getRangeTo(context.spawnPos));
        for (let i = 0; i < 43;) {
            if (!candidates[i])
                break;
            if (!last[candidates[i].x][candidates[i].y]) {
                last[candidates[i].x][candidates[i].y] = 'extension';
                ++i;
            }
        }
        return last;
    }

    @profile
    private fillExtensions(last: RoomBlueprint, context: PlanContext, seq: BuildSequence) {
        const { spawnPos, spawnFlood, terrain } = context;
        const considerList: RoomPosition[] = [];
        let templatesPut = 0;
        const tryArea = (limit: number) => {
            for (let x = spawnPos.x - limit; x <= spawnPos.x + limit; ++x) {
                for (let y = spawnPos.y - limit; y <= spawnPos.y + limit; ++y) {
                    if (templatesPut >= 8) {
                        throw 0;
                    }
                    if (this.canApply(last, x, y, EXTENSION_LAYOUT, terrain)) {
                        const applyPos = new RoomPosition(x, y, context.room.name);
                        if (spawnPos.getTravelDistanceTo(applyPos) >= spawnPos.getRangeTo(applyPos) * 1.2) {
                            considerList.push(applyPos);
                        }
                        else {
                            this.applyBlueprint(last, x, y, EXTENSION_LAYOUT, seq);
                            templatesPut++;
                        }
                    }
                }
            }
        };
        // ugly... needs refactoring
        try {
            tryArea(4);
            tryArea(7);
            tryArea(10);
            tryArea(13);
            tryArea(16);
            tryArea(19);
            tryArea(21);
            tryArea(24);
            tryArea(27);
        }
        catch (e) {
            if (e === 0) {
                return last;
            }
            else {
                throw e;
            }
        }
    }

    private putTowers(last: RoomBlueprint, context: PlanContext) {
        const { centerPos, room } = context;
        const candidates: RoomPosition[] = [];
        for (let i = 0; i < 50; ++i) {
            for (let j = 0; j < 50; ++j) {
                if (last[i][j] === undefined && context.terrain.get(i, j) !== TERRAIN_MASK_WALL) {
                    candidates.push(new RoomPosition(i, j, room.name));
                }
            }
        }
        candidates.sort((a, b) => a.getRangeTo(centerPos) - b.getRangeTo(centerPos));
        for (let i = 0; i < 6; ++i) {
            if (!candidates[i]) {
                return last;
            }
            last[candidates[i].x][candidates[i].y] = 'tower';
        }
        return last;
    }

    getRampartBlueprint(last: RoomBlueprint, context: PlanContext) {
        const poses = minCutWalls({
            roomSize: 50,
            isWall(pos) {
                return context.terrain.get(pos[0], pos[1]) === TERRAIN_MASK_WALL;
            },
            isCenter(pos) {
                return !!last[pos[0]][pos[1]];
            }
        });
        poses.forEach(pos => {
            const [x, y] = pos;
            last[x][y] = 'rampart';
        });
        return last;
    }

    paveRoads(last: RoomBlueprint, context: PlanContext, labPos: RoomPosition) {
        const { room } = context;
        context.roadPos = [];
        // 设置 cost matrix
        const roadMatrix = new PathFinder.CostMatrix;
        for (let x = 0; x < 50; ++x) {
            for (let y = 0; y < 50; ++y) {
                // 设置地形
                if (context.terrain.get(x, y) === TERRAIN_MASK_WALL) {
                    roadMatrix.set(x, y, 255);
                }
                else if (context.terrain.get(x, y) === TERRAIN_MASK_SWAMP) {
                    roadMatrix.set(x, y, 16);
                }
                else {
                    roadMatrix.set(x, y, 4);
                }
                // 有不可通过的建筑
                switch (last[x][y]) {
                    case STRUCTURE_ROAD:
                        roadMatrix.set(x, y, 1);
                        break;
                    case STRUCTURE_RAMPART:
                        roadMatrix.set(x, y, 6);
                        break;
                    case undefined:
                        break;
                    default:
                        roadMatrix.set(x, y, 255);
                }
            }
        }
        // 到 source 的路
        room.getSources().forEach(s => {
            const path = context.spawnPos.findPathTo(s.pos, {
                costCallback(roomName, costMatrix) {
                    return roadMatrix;
                },
                range: 2
            });
            path.forEach(step => {
                last[step.x][step.y] = 'road';
                context.roadPos.push(`${step.x}/${step.y}`);
            });
        });
        // 到 lab 的路
        context.spawnPos.findPathTo(labPos.x + 1, labPos.y + 1, {
            costCallback(roomName, costMatrix) {
                return roadMatrix;
            }
        }).forEach(step => {
            last[step.x][step.y] = 'road';
            // context.roadPos.push(`${step.x}/${step.y}`);
        });
        return last;
    }

    visualize(room: Room) {
        const blueprint = this.plan(room).blueprint;
        const visual = new RoomVisual2;
        for (let i = 0; i < blueprint.length; ++i) {
            for (let j = 0; j < blueprint[i].length; ++j) {
                if (!blueprint[i][j]) {
                    continue;
                }
                visual.structure(i, j, blueprint[i][j]);
            }
        }
        visual.connRoads();
    }

}